#ifndef MOONLIGHT_OHOS_NAPIUTILS_H
#define MOONLIGHT_OHOS_NAPIUTILS_H

#include <napi/native_api.h>
#include <cstdint>
#include <span>
#include <string>
#include <sstream>

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN  0x0
#define LOG_TAG     "NapiUtils"
#include <hilog/log.h>

/**
 * Return `r` if napi call `expr` is not ok.
 *
 * You may use `napiCallResult` to return the code returned by `expr`.
 */
#define NAPI_RETURN_IF_NOT_OK(r, expr) \
    if (auto napiCallResult = (expr); napiCallResult != napi_ok) { \
        OH_LOG_ERROR(LOG_APP, "NAPI call %{public}s failed with %{public}d", #expr, napiCallResult); \
        return (r); \
    }

#define RETURN_IF_NULLOPT_OR_ASSIGN(r, recv, expr) \
    if (auto val = (expr); !val) {\
        return (r); \
    } else { \
        (recv) = *val; \
    }

#define SETUP_NAPI_ARGS_OR_RETURN(env, info, argsCnt, r) \
    size_t argc; \
    NAPI_RETURN_IF_NOT_OK(r, napi_get_cb_info(env, info, &argc, nullptr, nullptr, nullptr)); \
    if (argc != (argsCnt)) { \
        OH_LOG_ERROR(LOG_APP, "NAPI function implementation expects %{public}d arguments, but got %{public}d", \
            (argsCnt), argc); \
        return (r); \
    } \
    napi_value args[(argsCnt)] = { nullptr }; \
    NAPI_RETURN_IF_NOT_OK(r, napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));

#define SETUP_NAPI_ARGS(env, info, argsCnt) \
    constexpr const char *__funcname_setup_args = __PRETTY_FUNCTION__; \
    SETUP_NAPI_ARGS_OR_RETURN(env, info, argsCnt, [&] { \
        std::stringstream ss; \
        ss << __funcname_setup_args << " Failed to initialize args"; \
        napi_throw_error(env, "NapiError", ss.str().c_str()); \
        return nullptr; \
    }());

#define DEFINE_NAPI_ARG_OR_RETURN(type, var, env, arg, r) \
    type var{}; \
    RETURN_IF_NULLOPT_OR_ASSIGN(r, var, NapiUtils::getValue<type>(env, arg));

#define DEFINE_NAPI_ARG(type, var, env, arg) \
    constexpr const char *__funcname_define_##var = __PRETTY_FUNCTION__; \
    DEFINE_NAPI_ARG_OR_RETURN(type, var, env, arg, [&] { \
        std::stringstream ss; \
        ss << __funcname_define_##var << " Failed to extracting arg " << #var; \
        napi_throw_error(env, "IllegalArgument", ss.str().c_str()); \
        return nullptr; \
    }());

struct NapiUtils {
    static napi_value javascriptClassConstructor(napi_env env, napi_callback_info info);

public:
    template<typename T>
    static std::optional<T> getValue(napi_env env, napi_value value);

    template<>
    std::optional<int32_t> getValue(napi_env env, napi_value value) {
        int32_t i;
        NAPI_RETURN_IF_NOT_OK(std::nullopt, napi_get_value_int32(env, value, &i));
//        OH_LOG_DEBUG(LOG_APP, "Got NAPI int32_t value of %{public}d", i);
        return i;
    }

    template<>
    std::optional<uint32_t> getValue(napi_env env, napi_value value) {
        uint32_t i;
        NAPI_RETURN_IF_NOT_OK(std::nullopt, napi_get_value_uint32(env, value, &i));
//        OH_LOG_DEBUG(LOG_APP, "Got NAPI uint32_t value of %{public}d", i);
        return i;
    }

    template<>
    std::optional<int64_t> getValue(napi_env env, napi_value value) {
        int64_t i;
        NAPI_RETURN_IF_NOT_OK(std::nullopt, napi_get_value_int64(env, value, &i));
//        OH_LOG_DEBUG(LOG_APP, "Got NAPI int64_t value of %{public}d", i);
        return i;
    }

    template<>
    std::optional<bool> getValue(napi_env env, napi_value value) {
        bool b;
        NAPI_RETURN_IF_NOT_OK(std::nullopt, napi_get_value_bool(env, value, &b));
//        OH_LOG_DEBUG(LOG_APP, "Got NAPI bool value of %{public}d", b);
        return b;
    }

    template<>
    std::optional<double> getValue(napi_env env, napi_value value) {
        double d;
        NAPI_RETURN_IF_NOT_OK(std::nullopt, napi_get_value_double(env, value, &d));
//        OH_LOG_DEBUG(LOG_APP, "Got NAPI double value of %{public}f", d);
        return d;
    }

    template<>
    std::optional<std::string> getValue(napi_env env, napi_value value) {
        size_t length;
        NAPI_RETURN_IF_NOT_OK(std::nullopt, napi_get_value_string_latin1(env, value, nullptr, 0, &length));
        std::string str(length, '\0');
        NAPI_RETURN_IF_NOT_OK(std::nullopt, napi_get_value_string_latin1(env, value, str.data(), str.capacity(), nullptr));
//        OH_LOG_DEBUG(LOG_APP, "Got NAPI string value of %{public}s, length %{public}d", str.c_str(), length);
        return str;
    }

    template<>
    std::optional<std::span<uint8_t>> getValue(napi_env env, napi_value value) {
        size_t length;
        void *addr;
        NAPI_RETURN_IF_NOT_OK(std::nullopt, napi_get_typedarray_info(env, value, nullptr, &length, &addr, nullptr, nullptr));
//        OH_LOG_DEBUG(LOG_APP, "Got NAPI uint8 array value of length %{public}d", length);
        return std::span(reinterpret_cast<uint8_t*>(addr), length);
    }


    template<typename T>
    static napi_value toNapiValue(napi_env env, const T &data);

    template<>
    napi_value toNapiValue(napi_env env, const int32_t &data) {
        napi_value value;
        NAPI_RETURN_IF_NOT_OK(nullptr, napi_create_int32(env, data, &value));
        return value;
    }

    template<>
    napi_value toNapiValue(napi_env env, const int64_t &data) {
        napi_value value;
        NAPI_RETURN_IF_NOT_OK(nullptr, napi_create_int64(env, data, &value));
        return value;
    }

    template<>
    napi_value toNapiValue(napi_env env, const double &data) {
        napi_value value;
        NAPI_RETURN_IF_NOT_OK(nullptr, napi_create_double(env, data, &value));
        return value;
    }

    template<>
    napi_value toNapiValue(napi_env env, const std::string &data) {
        napi_value value;
        NAPI_RETURN_IF_NOT_OK(nullptr, napi_create_string_utf8(env, data.c_str(), NAPI_AUTO_LENGTH, &value));
        return value;
    }

    template<>
    napi_value toNapiValue(napi_env env, const std::function<napi_value(napi_env)> &data) {
        return data(env);
    }

    using NapiValueType = std::variant<int32_t, int64_t, double, std::string, std::function<napi_value(napi_env)>>;

public:
    class ThreadSafeFunction final {
    public:
        ThreadSafeFunction(napi_threadsafe_function tsfn);
        ~ThreadSafeFunction();
        napi_status operator()(void *data);

        class Arguments {
        public:
            Arguments(std::initializer_list<NapiValueType> l);
            virtual ~Arguments();
            virtual std::vector<napi_value> toNapiValues(napi_env env) const;
        private:
            const std::vector<NapiValueType> data;
        };

    private:
        napi_threadsafe_function tsfn;

        // NOTE copy has to be disabled otherwise may call release on tsfn more than expected
        ThreadSafeFunction(const ThreadSafeFunction &) = delete;
        ThreadSafeFunction &operator=(const ThreadSafeFunction &) = delete;
    };

    /**
     * Call JavaScript callback `jsCallback` with `data` as arguments.
     * @param env
     * @param jsCallback A JavaScript callback with signature of `(...args: object[]) => void`.
     * @param context Not used.
     * @param data Pointer to `ThreadSafeFunction::Argument`. Will be freed by this callback.
     */
    static void callJsCb(napi_env env, napi_value jsCallback, void *context, void *data);

};

#endif //MOONLIGHT_OHOS_NAPIUTILS_H
